﻿using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Media3D;
using _3DTools;
using Petzold.Media3D;

namespace WpfHeadtracking
{
    /// <summary>
    /// A simple way to use headtracking in your WPF apps. 
    /// Original headtracking idea by Johnny Lee Chung: http://www.cs.cmu.edu/~johnny/projects/wii/
    /// For easier integration, it's derived from 3D Tools' Viewport3DDecorator: http://codeplex.com/3dtools
    /// </summary>
    public class Headtracking3DDecorator : Viewport3DDecorator
    {
        #region Dependency Properties

        public static readonly DependencyProperty CameraDefaultLookDirectionProperty =
            DependencyProperty.Register("CameraDefaultLookDirection", typeof (Vector3D),
                                        typeof (Headtracking3DDecorator),
                                        new FrameworkPropertyMetadata(new Vector3D(0, 0, -1)));

        public static readonly DependencyProperty CameraDefaultPositionProperty =
            DependencyProperty.Register("CameraDefaultPosition", typeof (Point3D), typeof (Headtracking3DDecorator),
                                        new FrameworkPropertyMetadata(new Point3D(0, 0, 1)));

        public static readonly DependencyProperty HeadtrackingProviderProperty =
            DependencyProperty.Register("HeadtrackingProvider", typeof (IHeadtrackingProvider),
                                        typeof (Headtracking3DDecorator), new FrameworkPropertyMetadata(null));

        public static readonly DependencyProperty TargetPositionProperty =
            DependencyProperty.Register("TargetPosition", typeof (Point3D), typeof (Headtracking3DDecorator),
                                        new UIPropertyMetadata(new Point3D(0, 0, 0)));

        #endregion

        #region Fields

        private readonly MatrixTransform3D matrix;
        private readonly TranslateTransform3D translate = new TranslateTransform3D();

        //Initially we consider the head 2 "screen heights" far from the screen
        private Point3D headOriginPosition = new Point3D(0, 0, 2);
        private bool initialized;
        private double screenHeightInWPF;

        #endregion

        #region Constructors

        static Headtracking3DDecorator()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (Headtracking3DDecorator),
                                                     new FrameworkPropertyMetadata(typeof (Headtracking3DDecorator)));
        }

        public Headtracking3DDecorator()
        {
            translate = new TranslateTransform3D();
            matrix = new MatrixTransform3D(Matrix3D.Identity);

            Loaded += Headtracking3DDecorator_Loaded;
        }

        #endregion

        #region Properties

        public Point3D CameraDefaultPosition
        {
            get { return (Point3D) GetValue(CameraDefaultPositionProperty); }
            set { SetValue(CameraDefaultPositionProperty, value); }
        }

        public Vector3D CameraDefaultLookDirection
        {
            get { return (Vector3D) GetValue(CameraDefaultLookDirectionProperty); }
            set { SetValue(CameraDefaultLookDirectionProperty, value); }
        }

        public Point3D TargetPosition
        {
            get { return (Point3D) GetValue(TargetPositionProperty); }
            set { SetValue(TargetPositionProperty, value); }
        }

        public IHeadtrackingProvider HeadtrackingProvider
        {
            get { return (IHeadtrackingProvider) GetValue(HeadtrackingProviderProperty); }
            set { SetValue(HeadtrackingProviderProperty, value); }
        }

        #endregion

        #region Event Handlers

        private void Headtracking3DDecorator_Loaded(object sender, RoutedEventArgs e)
        {
            HeadtrackingProvider.HeadPositionChanged += HeadtrackingProvider_HeadPositionChanged;
            HeadtrackingProvider.StartHeadtracking();
        }


        private void HeadtrackingProvider_HeadPositionChanged(object sender,
                                                              PropertyChangedEventArgs e)
        {
            if (HeadtrackingProvider.IsHeadtracking)
                if (initialized)
                {
                    SetupTransforms();
                }
                else
                    InitializeConfiguration();
        }

        #endregion

        #region Helper Methods

        private void InitializeConfiguration()
        {
            Viewport3D viewport3D = Viewport3D;

            if (viewport3D.ActualWidth*viewport3D.ActualHeight == 0)
            {
                initialized = false;
                return;
            }

            LineRange range;
            ViewportInfo.Point2DtoPoint3D(viewport3D, new Point(0, 0), out range);
            Point3D cornerTopLeft = range.PointFromZ(0);
            ViewportInfo.Point2DtoPoint3D(viewport3D, new Point(viewport3D.ActualWidth, viewport3D.ActualHeight),
                                          out range);
            Point3D cornerBottomRight = range.PointFromZ(0);

            screenHeightInWPF = (cornerTopLeft.Y - cornerBottomRight.Y);

            Camera cam = viewport3D.Camera;
            if (cam.Transform is Transform3DGroup)
            {
                (cam.Transform as Transform3DGroup).Children.Add(matrix);
                (cam.Transform as Transform3DGroup).Children.Add(translate);
            }
            else
            {
                var t3dg = new Transform3DGroup();
                t3dg.Children.Add(cam.Transform);
                t3dg.Children.Add(matrix);
                t3dg.Children.Add(translate);
                cam.Transform = t3dg;
            }

            initialized = true;
        }

        private void SetupTransforms()
        {
            Point3D headPosition = HeadtrackingProvider.HeadPosition;

            Point3D headPositionInWPF = GetPositionInWPF(headPosition);
            Point3D headOriginPositionInWPF = GetPositionInWPF(headOriginPosition);

            matrix.Matrix = PerspectiveOffCenterMultiply(headPositionInWPF.X/headPositionInWPF.Z,
                                                         -headPositionInWPF.Y/headPositionInWPF.Z, 0.05);

            translate.OffsetX = headPositionInWPF.X - headOriginPositionInWPF.X;
            translate.OffsetY = -headPositionInWPF.Y - headOriginPositionInWPF.Y;
            translate.OffsetZ = headPositionInWPF.Z - headOriginPositionInWPF.Z;
        }

        private Point3D GetPositionInWPF(Point3D position)
        {
            Viewport3D viewport3D = Viewport3D;
            LineRange range;
            ViewportInfo.Point2DtoPoint3D(viewport3D,
                                          new Point((-position.X + 0.5)*Viewport3D.ActualHeight,
                                                    (-position.Y + 0.5)*Viewport3D.ActualHeight), out range);
            return new Point3D(position.X*screenHeightInWPF, position.Y*screenHeightInWPF,
                               position.Z*screenHeightInWPF);
        }


        private static Matrix3D PerspectiveOffCenterMultiply(double x, double y, double near)
        {
            // Formula based in http://www.ogre3d.org/forums/viewtopic.php?p=266734#p266734

            return new Matrix3D(1, 0, x, x*near,
                                0, 1, y, y*near,
                                0, 0, 1, 0,
                                0, 0, 0, 1);
        }

        #endregion

        #region Public Methods

        public void ResetCameraCenter()
        {
            HeadtrackingProvider.SetHeadOrigin();

            headOriginPosition = HeadtrackingProvider.HeadPosition;
        }

        #endregion
    }
}